package com.dbflow5.runtime;

import com.dbflow5.config.DBFlowDatabase;
import com.dbflow5.config.FlowLog;
import com.dbflow5.database.DatabaseWrapper;
import com.dbflow5.structure.Model;
import com.dbflow5.transaction.ProcessModelTransaction;
import com.dbflow5.transaction.Transaction;
import ohos.aafwk.ability.DataAbilityRemoteException;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.BiFunction;

/**
 * Description: This queue will bulk save items added to it when it gets access to the DB. It should only exist as one entity.
 * It will save the [.MODEL_SAVE_SIZE] at a time or more only when the limit is reached. It will not
 */
public class DBBatchSaveQueue extends Thread {

    /**
     * Once the queue size reaches 50 or larger, the thread will be interrupted and we will batch save the models.
     */
    private static final int MODEL_SAVE_SIZE = 50;

    /**
     * The default time that it will awake the save queue thread to check if any models are still waiting to be saved
     */
    private static final int sMODEL_SAVE_CHECK_TIME = 30000;

    private DBFlowDatabase databaseDefinition;

    /**
     * Tells how many items to save at a time. This can be set using [.setModelSaveSize]
     */
    private int modelSaveSize = MODEL_SAVE_SIZE;

    /**
     * Sets the time we check periodically for leftover DB objects in our queue to save.
     */
    private long modelSaveCheckTime = sMODEL_SAVE_CHECK_TIME;

    /**
     * The list of DB objects that we will save here
     */
    private final List<Object> models = new ArrayList<>();

    /**
     * If true, this queue will quit.
     */
    private boolean isQuitting = false;

    private BiFunction<Transaction<Void>, Throwable, Void> errorListener = null;
    private BiFunction<Transaction<Void>, Void, Void> successListener = null;
    private Runnable emptyTransactionListener = null;

    public DBBatchSaveQueue(DBFlowDatabase databaseDefinition){
        this.databaseDefinition = databaseDefinition;
    }

    private final ProcessModelTransaction.ProcessModel<Object> modelSaver = ProcessModelTransaction.processModel(new BiFunction<Object, DatabaseWrapper, Void>() {
        @Override
        public Void apply(Object model, DatabaseWrapper databaseWrapper) {
            if(model instanceof Model){
                try {
                    ((Model)model).save(databaseDefinition);
                } catch (DataAbilityRemoteException e) {
                    e.printStackTrace();
                }
            }else {
                Model.save((Class<Object>) model.getClass(), model, databaseDefinition);
            }
            return null;
        }
    });

    private final BiFunction<Transaction<Void>, Void, Void> successCallback = new BiFunction<Transaction<Void>, Void, Void>() {
        @Override
        public Void apply(Transaction<Void> transaction, Void result) {
            successListener.apply(transaction, result);
            return null;
        }
    };

    private final BiFunction<Transaction<Void>, Throwable, Void> errorCallback = new BiFunction<Transaction<Void>, Throwable, Void>() {
        @Override
        public Void apply(Transaction<Void> transaction, Throwable error) {
            errorListener.apply(transaction, error);
            return null;
        }
    };

    /**
     * Sets how many models to save at a time in this queue.
     * Increase it for larger batches, but slower recovery time.
     * Smaller the batch, the more time it takes to save overall.
     * @param mModelSaveSize mModelSaveSize
     */
    public void setModelSaveSize(int mModelSaveSize) {
        this.modelSaveSize = mModelSaveSize;
    }

    /**
     * Sets how long, in millis that this queue will check for leftover DB objects that have not been saved yet.
     * The default is [.sMODEL_SAVE_CHECK_TIME]
     *
     * @param time The time, in millis that queue automatically checks for leftover DB objects in this queue.
     */
    public void setModelSaveCheckTime(long time) {
        this.modelSaveCheckTime = time;
    }

    /**
     * Listener for errors in each batch [Transaction]. Called from the DBBatchSaveQueue thread.
     *
     * @param errorListener The listener to use.
     */
    public void setErrorListener(BiFunction<Transaction<Void>, Throwable, Void> errorListener) {
        this.errorListener = errorListener;
    }

    /**
     * Listener for batch updates. Called from the DBBatchSaveQueue thread.
     *
     * @param successListener The listener to get notified when changes are successful.
     */
    public void setSuccessListener(BiFunction<Transaction<Void>, Void, Void> successListener) {
        this.successListener = successListener;
    }

    /**
     * Listener for when there is no work done. Called from the DBBatchSaveQueue thread.
     *
     * @param emptyTransactionListener The listener to get notified when the save queue thread ran but was empty.
     */
    public void setEmptyTransactionListener(Runnable emptyTransactionListener) {
        this.emptyTransactionListener = emptyTransactionListener;
    }

    @Override
    public void run() {
        super.run();
        //Looper.prepare();
        //Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);
        while (true) {
            List<Object> tmpModels = null;
            synchronized(models) {
                tmpModels = new ArrayList<>(models);
                models.clear();
            }
            if (!tmpModels.isEmpty()) {
                ProcessModelTransaction.Builder<Object> builder = new ProcessModelTransaction.Builder<>(modelSaver);
                tmpModels.forEach(builder::add);
                databaseDefinition.beginTransactionAsync(builder.build())
                        .success(successCallback)
                        .error(errorCallback)
                        .build()
                        .execute();
            } else {
                emptyTransactionListener.run();
            }

            try {
                //sleep, and then check for leftovers
                Thread.sleep(modelSaveCheckTime);
            } catch (InterruptedException e) {
                FlowLog.log(FlowLog.Level.I, "DBRequestQueue Batch interrupted to start saving");
            }

            if (isQuitting) {
                return;
            }
        }
    }

    /**
     * Will cause the queue to wake from sleep and handle it's current list of items.
     */
    public void purgeQueue() {
        interrupt();
    }

    /**
     * Adds an object to this queue.
     * @param inModel inModel
     */
    public void add(Object inModel) {
        synchronized(models) {
            models.add(inModel);

            if (models.size() > modelSaveSize) {
                interrupt();
            }
        }
    }

    /**
     * Adds a [Collection] of DB objects to this queue
     * @param list list
     */
    public void addAll(Collection<Object> list) {
        synchronized(models) {
            models.addAll(list);

            if (models.size() > modelSaveSize) {
                interrupt();
            }
        }
    }

    /**
     * Adds a [Collection] of class that extend Object to this queue
     * @param list list
     */
    public void addAll2(Collection<Object> list) {
        synchronized(models) {
            models.addAll(list);

            if (models.size() > modelSaveSize) {
                interrupt();
            }
        }
    }

    /**
     * Removes a DB object from this queue before it is processed.
     * @param outModel outModel
     */
    public void remove(Object outModel) {
        synchronized(models) {
            models.remove(outModel);
        }
    }

    /**
     * Removes a [Collection] of DB object from this queue
     * before it is processed.
     * @param outCollection outCollection
     */
    public void removeAll(Collection<Object> outCollection) {
        synchronized(models) {
            models.removeAll(outCollection);
        }
    }

    /**
     * Removes a [Collection] of DB objects from this queue
     * before it is processed.
     * @param outCollection outCollection
     */
    public void removeAll2(Collection<?> outCollection) {
        synchronized(models) {
            models.removeAll(outCollection);
        }
    }

    /**
     * Quits this queue after it sleeps for the [.modelSaveCheckTime]
     */
    public void quit() {
        isQuitting = true;
    }
}

